﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using Tesla.Core;
using Tesla.Graphics;
using Tesla.Graphics.Implementation;
using Tesla.Util;
using D3D = SlimDX.Direct3D10;
using DXGI = SlimDX.DXGI;
using SDX = SlimDX;

namespace Tesla.Direct3D10.Graphics.Implementation {
    /// <summary>
    /// Concrete Direct3D10 implementation for <see cref="IndexBuffer"/>.
    /// </summary>
    public sealed class D3D10IndexBufferImplementation : IndexBufferImplementation {
        private D3D10Renderer _renderer;
        private D3D.Device _graphicsDevice;
        private D3D.Buffer _buffer;
        private D3D.Buffer _staging;

        /// <summary>
        /// Gets the D3D10Renderer that created this implementation.
        /// </summary>
        internal D3D10Renderer Renderer {
            get {
                return _renderer;
            }
        }

        /// <summary>
        /// Gets the D3D10 Index Buffer.
        /// </summary>
        internal D3D.Buffer D3DIndexBuffer {
            get {
                return _buffer;
            }
        }

        /// <summary>
        /// Gets the D3D10 Index Format.
        /// </summary>
        internal DXGI.Format D3DIndexFormat {
            get {
                return (base.IndexFormat == IndexFormat.ThirtyTwoBits) ? DXGI.Format.R32_UInt : DXGI.Format.R16_UInt;
            }
        }

        /// <summary>
        /// Creates a new instance of <see cref="D3D10IndexBufferImplementation"/>.
        /// </summary>
        /// <param name="renderer">The D3D renderer.</param>
        /// <param name="format">The index format.</param>
        /// <param name="indexCount">The number of indices.</param>
        /// <param name="usage">The buffer usage specifying what type of memory the buffer should be created in.</param>
        internal D3D10IndexBufferImplementation(D3D10Renderer renderer, IndexFormat format, int indexCount, ResourceUsage usage)
            : base(format, indexCount, usage) {
            _renderer = renderer;
            _graphicsDevice = _renderer.GraphicsDevice;

            int formatBytes = (format == IndexFormat.ThirtyTwoBits) ? 4 : 2;

            try {
                //Determine appropiate vertex buffer to create
                if(usage == ResourceUsage.Static) {
                    D3D.ResourceUsage rUsage = D3D.ResourceUsage.Default;
                    D3D.CpuAccessFlags cpuAccess = D3D.CpuAccessFlags.None;
                    _buffer = new D3D.Buffer(_graphicsDevice, new D3D.BufferDescription(indexCount * formatBytes, rUsage, D3D.BindFlags.IndexBuffer, cpuAccess, D3D.ResourceOptionFlags.None));
                } else if(usage == ResourceUsage.Dynamic) {
                    D3D.ResourceUsage rUsage = D3D.ResourceUsage.Dynamic;
                    D3D.CpuAccessFlags cpuAccess = D3D.CpuAccessFlags.Write;
                    _buffer = new D3D.Buffer(_graphicsDevice, new D3D.BufferDescription(indexCount * formatBytes, rUsage, D3D.BindFlags.IndexBuffer, cpuAccess, D3D.ResourceOptionFlags.None));
                }

                //Add to tracker
                _renderer.Resources.AddTrackedObject(_buffer.ComPointer, this);
            } catch(Exception e) {
                Dispose();
                throw new TeslaException("Error creating DX10 buffer:\n" + e.Message);
            }
        }

        /// <summary>
        /// Creates a new instance of <see cref="D3D10IndexBufferImplementation"/>.
        /// </summary>
        /// <param name="renderer">The D3D renderer.</param>
        /// <param name="indices">The index data</param>
        /// <param name="usage">The buffer usage specifying what type of memory the buffer should be created in.</param>
        internal D3D10IndexBufferImplementation(D3D10Renderer renderer, DataBuffer<int> indices, ResourceUsage usage)
            : base(indices, usage) {

            if(indices == null) {
                throw new ArgumentNullException("indices", "Indices cannot be null.");
            }

            _renderer = renderer;
            _graphicsDevice = _renderer.GraphicsDevice;

            try {
                //Determine appropiate vertex buffer to create
                if(usage == ResourceUsage.Static) {
                    D3D.ResourceUsage rUsage = D3D.ResourceUsage.Default;
                    D3D.CpuAccessFlags cpuAccess = D3D.CpuAccessFlags.None;
                    using(SDX.DataStream ds = new SDX.DataStream(indices.Buffer, true, true)) {
                        _buffer = new D3D.Buffer(_graphicsDevice, ds, new D3D.BufferDescription(indices.SizeInBytes, rUsage, D3D.BindFlags.IndexBuffer, cpuAccess, D3D.ResourceOptionFlags.None));
                    }
                } else if(usage == ResourceUsage.Dynamic) {
                    D3D.ResourceUsage rUsage = D3D.ResourceUsage.Dynamic;
                    D3D.CpuAccessFlags cpuAccess = D3D.CpuAccessFlags.Write;
                    using(SDX.DataStream ds = new SDX.DataStream(indices.Buffer, true, true)) {
                        _buffer = new D3D.Buffer(_graphicsDevice, ds, new D3D.BufferDescription(indices.SizeInBytes, rUsage, D3D.BindFlags.IndexBuffer, cpuAccess, D3D.ResourceOptionFlags.None));
                    }
                }

                //Add to tracker
                _renderer.Resources.AddTrackedObject(_buffer.ComPointer, this);
            } catch(D3D.Direct3D10Exception e) {
                Dispose();
                throw new TeslaException("Error creating D3D10 buffer.", e);
            }
        }

        /// <summary>
        /// Creates a new instance of <see cref="D3D10IndexBufferImplementation"/>.
        /// </summary>
        /// <param name="renderer">The D3D renderer.</param>
        /// <param name="indices">The index data</param>
        /// <param name="usage">The buffer usage specifying what type of memory the buffer should be created in.</param>
        internal D3D10IndexBufferImplementation(D3D10Renderer renderer, DataBuffer<short> indices, ResourceUsage usage)
            : base(indices, usage) {
            if(indices == null) {
                throw new ArgumentNullException("indices", "Indices cannot be null.");
            }
            _renderer = renderer;
            _graphicsDevice = _renderer.GraphicsDevice;

            try {
                //Determine appropiate vertex buffer to create
                if(usage == ResourceUsage.Static) {
                    D3D.ResourceUsage rUsage = D3D.ResourceUsage.Default;
                    D3D.CpuAccessFlags cpuAccess = D3D.CpuAccessFlags.None;
                    using(SDX.DataStream ds = new SDX.DataStream(indices.Buffer, true, true)) {
                        _buffer = new D3D.Buffer(_graphicsDevice, ds, new D3D.BufferDescription(indices.SizeInBytes, rUsage, D3D.BindFlags.IndexBuffer, cpuAccess, D3D.ResourceOptionFlags.None));
                    }
                } else if(usage == ResourceUsage.Dynamic) {
                    D3D.ResourceUsage rUsage = D3D.ResourceUsage.Dynamic;
                    D3D.CpuAccessFlags cpuAccess = D3D.CpuAccessFlags.Write;
                    using(SDX.DataStream ds = new SDX.DataStream(indices.Buffer, true, true)) {
                        _buffer = new D3D.Buffer(_graphicsDevice, ds, new D3D.BufferDescription(indices.SizeInBytes, rUsage, D3D.BindFlags.IndexBuffer, cpuAccess, D3D.ResourceOptionFlags.None));
                    }
                }

                //Add to tracker
                _renderer.Resources.AddTrackedObject(_buffer.ComPointer, this);
            } catch(D3D.Direct3D10Exception e) {
                Dispose();
                throw new TeslaException("Error creating D3D10 buffer.", e);
            }
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="D3D10IndexBufferImplementation"/> is reclaimed by garbage collection.
        /// </summary>
        ~D3D10IndexBufferImplementation() {
            Dispose(false);
        }

        private void CreateStaging() {
            if(_staging == null) {
                D3D.BufferDescription desc = new D3D.BufferDescription();
                desc.BindFlags = D3D.BindFlags.None;
                desc.CpuAccessFlags = D3D.CpuAccessFlags.Write | D3D.CpuAccessFlags.Read;
                desc.OptionFlags = D3D.ResourceOptionFlags.None;
                desc.SizeInBytes = base.IndexCount * ((base.IndexFormat == IndexFormat.SixteenBits) ? 2 : 4);
                desc.Usage = D3D.ResourceUsage.Staging;
                _staging = new D3D.Buffer(_graphicsDevice, desc);

                //Add to tracker
                _renderer.Resources.AddTrackedObject(_staging.ComPointer, this);
            }
        }

        /// <summary>
        /// Writes data from the array to the index buffer.
        /// </summary>
        /// <typeparam name="T">The type of data in the index buffer - int or short.</typeparam>
        /// <param name="data">Array to copy the data from</param>
        /// <param name="startIndex">Starting index in the array at which to start copying from</param>
        /// <param name="elementCount">Number of indices to write</param>
        /// <param name="offsetInBytes">Offset from the start of the index buffer at which to start writing at</param>
        /// <param name="writeOptions">Write options, used only if this is a dynamic buffer. None, discard, no overwrite</param>
        /// <remarks>See implementors for exceptions that may occur.</remarks>
        public override void SetData<T>(T[] data, int startIndex, int elementCount, int offsetInBytes, DataWriteOptions writeOptions) {
            if(_buffer == null || _buffer.Disposed) {
                throw new ObjectDisposedException(GetType().Name);
            }

            //Throws null or out of range exception
            D3D10Helper.CheckArrayBounds(data, startIndex, elementCount);

            int numBytes = MemoryHelper.SizeOf<T>();

            int ibSize = base.IndexCount * ((base.IndexFormat == IndexFormat.SixteenBits) ? 2 : 4);
            int dataSize = elementCount * numBytes;

            if(offsetInBytes < 0 || offsetInBytes > ibSize) {
                throw new ArgumentOutOfRangeException("offsetInBytes", "Byte offset is out of range.");
            }

            if((offsetInBytes + dataSize) > ibSize) {
                throw new ArgumentOutOfRangeException("data", "Byte offset and the number of elements to write will cause a buffer overflow.");
            }

            bool usesStaging = false;
            if(base.BufferUsage == ResourceUsage.Static || writeOptions == DataWriteOptions.None) {
                CreateStaging();
                usesStaging = true;
            }

            try {
                if(usesStaging) {
                    using(SDX.DataStream ds = _staging.Map(D3D.MapMode.Write, D3D.MapFlags.None)) {
                        ds.Position = offsetInBytes;
                        ds.WriteRange<T>(data, startIndex, elementCount);
                        _staging.Unmap();

                        //If we're writing to the entire IB just copy the whole thing
                        if(offsetInBytes == 0 && startIndex == 0 && dataSize == ibSize) {
                            _graphicsDevice.CopyResource(_staging, _buffer);
                        } else {
                            D3D.ResourceRegion region = new D3D.ResourceRegion();
                            region.Left = offsetInBytes;
                            region.Right = offsetInBytes + dataSize;
                            region.Front = region.Top = 0;
                            region.Back = region.Bottom = 1;
                            _graphicsDevice.CopySubresourceRegion(_staging, 0, region, _buffer, 0, offsetInBytes, 0, 0);
                        }
                    }
                } else {
                    D3D.MapMode mode = (writeOptions == DataWriteOptions.Discard) ? D3D.MapMode.WriteDiscard : D3D.MapMode.WriteNoOverwrite;
                    using(SDX.DataStream ds = _buffer.Map(mode, D3D.MapFlags.None)) {
                        ds.Position = offsetInBytes;
                        ds.WriteRange<T>(data, startIndex, elementCount);
                        _buffer.Unmap();
                    }
                }
            } catch(Exception e) {
                throw new TeslaException("Error writing to D3D10 Buffer.", e);
            }
        }

        /// <summary>
        /// Reads data from the index buffer into the array.
        /// </summary>
        /// <typeparam name="T">The type of data in the index buffer - int or short.</typeparam>
        /// <param name="data">Array to write the data to</param>
        /// <param name="startIndex">Starting index in the array at which to start writing to</param>
        /// <param name="elementCount">Number of indices to read</param>
        /// <param name="offsetInBytes">Offset from the start of the index buffer at which to start copying from</param>
        /// <remarks>See implementors for exceptions that may occur.</remarks>
        public override void GetData<T>(T[] data, int startIndex, int elementCount, int offsetInBytes) {
            if(_buffer == null || _buffer.Disposed) {
                throw new ObjectDisposedException(GetType().Name);
            }

            //Throws null or out of range exception
            D3D10Helper.CheckArrayBounds(data, startIndex, elementCount);

            int numBytes = MemoryHelper.SizeOf<T>();

            int ibSize = base.IndexCount * ((base.IndexFormat == IndexFormat.SixteenBits) ? 2 : 4);
            int dataSize = elementCount * numBytes;

            if(offsetInBytes < 0 || offsetInBytes > ibSize) {
                throw new ArgumentOutOfRangeException("offsetInBytes", "Byte offset is out of range.");
            }

            if((offsetInBytes + dataSize) > ibSize) {
                throw new ArgumentOutOfRangeException("data", "Byte offset and the number of elements to read will cause a buffer overflow.");
            }

            CreateStaging();

            try {
                //If we're reading from the entire VB just copy the whole thing
                if(offsetInBytes == 0 && startIndex == 0 && dataSize == ibSize) {
                    _graphicsDevice.CopyResource(_buffer, _staging);
                } else {
                    D3D.ResourceRegion region = new D3D.ResourceRegion();
                    region.Left = offsetInBytes;
                    region.Right = offsetInBytes + dataSize;
                    region.Front = region.Top = 0;
                    region.Back = region.Bottom = 1;
                    _graphicsDevice.CopySubresourceRegion(_buffer, 0, region, _staging, 0, offsetInBytes, 0, 0);
                }

                using(SDX.DataStream ds = _staging.Map(D3D.MapMode.Read, D3D.MapFlags.None)) {
                    ds.Position = offsetInBytes;
                    ds.ReadRange<T>(data, startIndex, elementCount);
                    _staging.Unmap();
                }
            } catch(Exception e) {
                throw new TeslaException("Error reading from D3D10 Buffer.", e);
            }
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected override void Dispose(bool disposing) {
            if(!IsDisposed) {
                //Dispose of managed resources
                if(disposing) {
                    if(_buffer != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_buffer.ComPointer);
                        }
                        _buffer.Dispose();
                        _buffer = null;
                    }
                    if(_staging != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_staging.ComPointer);
                        }
                        _staging.Dispose();
                        _staging = null;
                    }
                }
                base.Dispose(disposing);
            }
        }
    }
}
